////////////////////////////////////////////////////////////////////////////
//
//  CryEngine Source File.
//  Copyright (C), Crytek.
// -------------------------------------------------------------------------
//  File name:   PlatformOS_PC.cpp
//  Created:     11/02/2010 by Alex McCarthy.
//  Description: Implementation of the IPlatformOS interface for PC
// -------------------------------------------------------------------------
//  History:
//
////////////////////////////////////////////////////////////////////////////

#include <StdAfx.h>

#if !defined(XENON) && !defined(PS3) && !defined(gringo) && !defined(CAFE)

#if defined(WIN32) || defined(WIN64)
#define PLATFORM_WINDOWS
#endif

#include "PlatformOS_PC.h"
#include "SaveReaderWriter_CryPak.h"
#include "ICryPak.h"

//To get USE_CRYLOBBY_GAMESPY #define
#include "ICryLobby.h"

#if !defined(LINUX)
#include <WinSock2.h>
#endif


#include "../ZipDir.h"
#include "../CryArchive.h"
#include "INetwork.h"
#include <IZLibCompressor.h>
#include <StringUtils.h>

IPlatformOS* IPlatformOS::Create( const uint8 createParams )
{
	return new CPlatformOS_PC( createParams );
}

CPlatformOS_PC::CPlatformOS_PC( const uint8 createParams )
: m_listeners(4)
, m_fpsWatcher(15.0f, 3.0f, 7.0f)
, m_delayLevelStartIcon(0.0f)
, m_bSignedIn(false)
, m_bSaving(false)
, m_bAllowMessageBox(true)
, m_bLevelLoad(false)
, m_bSaveDuringLevelLoad(false)
{
	m_bAllowMessageBox = (GetISystem()->GetICmdLine()->FindArg(eCLAT_Pre, "noprompt") == NULL);
	AddListener(this, "PC");

	//TODO: Handle early corruption detection (createParams & IPlatformOS::eCF_EarlyCorruptionDetected ) if necessary.
}

void CPlatformOS_PC::Tick(float realFrameTime)
{
	if(m_delayLevelStartIcon)
	{
		m_delayLevelStartIcon -= realFrameTime;
		if(m_delayLevelStartIcon <= 0.0f)
		{
			m_delayLevelStartIcon = 0.0f;

			IPlatformOS::SPlatformEvent event(0);
			event.m_eEventType = IPlatformOS::SPlatformEvent::eET_FileWrite;
			event.m_uParams.m_fileWrite.m_type = SPlatformEvent::eFWT_SaveStart;
			NotifyListeners(event);

			event.m_eEventType = IPlatformOS::SPlatformEvent::eET_FileWrite;
			event.m_uParams.m_fileWrite.m_type = SPlatformEvent::eFWT_SaveEnd;
			NotifyListeners(event);
		}
	}

	SaveDirtyFiles();
}

void CPlatformOS_PC::OnPlatformEvent(const IPlatformOS::SPlatformEvent& _event)
{
	switch(_event.m_eEventType)
	{
	case SPlatformEvent::eET_FileWrite:
		{
			if(_event.m_uParams.m_fileWrite.m_type == SPlatformEvent::eFWT_CheckpointLevelStart)
			{
				m_delayLevelStartIcon = 5.0f;
			}
			break;
		}
	}
}

bool CPlatformOS_PC::GetUserProfilePreference(unsigned int user, EUserProfilePreference ePreference, SUserProfileVariant& outResult) const
{
	return false;
}

unsigned int CPlatformOS_PC::UserGetMaximumSignedInUsers() const
{
	return 1;
}

bool CPlatformOS_PC::UserIsSignedIn(unsigned int userIndex) const
{
	return userIndex != Unknown_User && m_bSignedIn;
}

bool CPlatformOS_PC::UserDoSignIn(unsigned int numUsersRequested, unsigned int /*controllerIndex*/)
{
	if(!m_bSignedIn)
	{
		m_bSignedIn = true;

		// Tell the system that we are signed in
		IPlatformOS::SPlatformEvent event(0);
		event.m_eEventType = SPlatformEvent::eET_SignIn;
		event.m_uParams.m_signIn.m_previousSignedInState = SPlatformEvent::eSIS_NotSignedIn;
		event.m_uParams.m_signIn.m_signedInState = IPlatformOS::SPlatformEvent::eSIS_SignedInLocally; // TODO: we may need to pass live
		NotifyListeners(event);

		// Tell the system that storage is mounted - required for CGame
		event.m_eEventType = SPlatformEvent::eET_StorageMounted;
		event.m_uParams.m_storageMounted.m_bPhysicalMedia = true;
		event.m_uParams.m_storageMounted.m_bOnlyUpdateMediaState = false;
		NotifyListeners(event);
	}
	return true;
}

bool CPlatformOS_PC::UserGetName(unsigned int userIndex, IPlatformOS::TUserName& outName) const
{
#ifdef PLATFORM_WINDOWS
	DWORD numChars = outName.MAX_SIZE;
	std::vector<wchar_t> nameW(numChars);
	BOOL e = GetUserNameW(&nameW[0], &numChars);
	nameW[numChars] = 0;
	// UNICODE to UTF-8 for correct file system operation
	CryStringUtils::WStrToUTF8(wstring(&nameW[0]), outName);
	return m_bSignedIn && (e ? true : false);
#else
	outName.assign( gEnv->pSystem->GetLoggedInUserName() );
	return true;
#endif
}

bool CPlatformOS_PC::UserGetOnlineName(unsigned int userIndex, IPlatformOS::TUserName& outName) const
{
#if USE_CRYLOBBY_GAMESPY	
	//JAT - this temporary code for development waiting for the proper PC installer and profile registration service for GameSpy ID
	ICVar* pGameSpyNickCVar = gEnv->pConsole->GetCVar("g_gamespy_unique_nick");
	if (pGameSpyNickCVar)
	{
		const char* const gamespyNick = pGameSpyNickCVar->GetString();
		//Only use the cvar value if it isn't blank
		if (gamespyNick[0] != '\0')
		{
			outName.assign(gamespyNick);
			return true;
		}
	}
#endif // #if USE_CRYLOBBY_GAMESPY	
	// No distinction between offline and online names on pc at this time.
	return UserGetName(userIndex,outName);
}

// func returns true if the data is correctly encrypted and signed. caller is then responsible for calling delete[] on the returned data buffer
// returns false if there is a problem, caller has no data to free if false is returned
bool CPlatformOS_PC::DecryptAndCheckSigning(const char *pInData, int inDataLen, char **pOutData, int *pOutDataLen, const uint8 key[16])
{
	INetwork					*pNet=GetISystem()->GetINetwork();
	IZLibCompressor		*pZLib=GetISystem()->GetIZLibCompressor();
	TCipher						cipher=pNet->BeginCipher(key, 8);	// Use the first 8 bytes of the key as the decryption key
	char							*pOutput=NULL;
	int								outDataLen=0;
	bool							valid=false;

	if (inDataLen>16)
	{
		if (cipher)
		{
			pOutput=new char[inDataLen];
			if (pOutput)
			{
				pNet->Decrypt(cipher,(uint8*)pOutput,(const uint8*)pInData,inDataLen);

				// validate cksum to check for successful decryption and for data signing
				SMD5Context		ctx;
				char					digest[16];

				pZLib->MD5Init(&ctx);
				pZLib->MD5Update(&ctx,(const char *)(key+8),8);	// Use the last 8 bytes of the key as the signing key
				pZLib->MD5Update(&ctx,pOutput,inDataLen-16);
				pZLib->MD5Final(&ctx,digest);

				if (memcmp(digest,pOutput+inDataLen-16,16)!=0)
				{
					CryLog("MD5 fail on dlc xml");
				}
				else
				{
					CryLog("dlc xml passed decrypt and MD5 checks");
					valid=true;
				}
			}
			pNet->EndCipher(cipher);
		}
	}

	if (valid)
	{
		*pOutData=pOutput;
		*pOutDataLen=inDataLen-16;
	}
	else
	{
		delete [] pOutput;
		*pOutData=NULL;
		*pOutDataLen=0;
	}

	return valid;
}

void CPlatformOS_PC::MountDLCContent(IDLCListener* pCallback, unsigned int user, const uint8 keyData[16])
{
	//Get the DLC install directory
	const char* dlcDir = gEnv->pConsole->GetCVar("dlc_directory")->GetString();
	
	if( dlcDir == NULL || dlcDir[ 0 ] == '\0' )
	{
		//use default path of install root (above default access)
		dlcDir = "..";
	}

	int nDLCPacksFound = 0;

	// Make the dlc dir if it doesn't exist already
	bool bDirExists = gEnv->pCryPak->MakeDir(dlcDir);
	if (bDirExists)
	{
		CryFixedStringT<ICryPak::g_nMaxPath> findPath;
		findPath.Format( "%s/*", dlcDir );

		// Search for all subfolders with a file named dlc.sxml, this corresponds to a DLC package
		IFileFinderPtr fileFinder = GetFileFinder(user);
		_finddata_t fd;
		intptr_t nFS = fileFinder->FindFirst( findPath.c_str(), &fd);
		if (nFS != -1)
		{
			do
			{
				// Skip files, only want subdirectories
				if (!(fd.attrib & _A_SUBDIR) || !strcmp(fd.name, ".") || !strcmp(fd.name, ".."))
				{
					continue;
				}

				// Try to load the xml file
				stack_string path;
				path.Format("%s/%s/dlc.sxml", dlcDir, fd.name);
				CryLog( "DLC: Trying %s", path.c_str() );

				if (gEnv->pCryPak->IsFileExist(path.c_str()))
				{
					bool success = false;
					ICryPak* pCryPak = gEnv->pCryPak;
					FILE* pFile = pCryPak->FOpen(path.c_str(), "rb");
					if (pFile)
					{
						pCryPak->FSeek(pFile,0, SEEK_END);
						int size=pCryPak->FTell(pFile);
						pCryPak->FSeek(pFile, 0, SEEK_SET);
						char* pData = new char[size];
						pCryPak->FRead(pData, size, pFile);
						pCryPak->FClose(pFile);

						char* pDecryptedData = NULL;
						int decryptedSize = 0;
						if (DecryptAndCheckSigning(pData, size, &pDecryptedData, &decryptedSize, keyData))
						{
							XmlNodeRef rootNode = gEnv->pSystem->LoadXmlFromBuffer(pDecryptedData, decryptedSize);
							if (rootNode)
							{
								path.Format("%s/%s", dlcDir, fd.name);
								pCallback->OnDLCMounted(rootNode, path.c_str());
								success = true;

								nDLCPacksFound++;
							}
							delete [] pDecryptedData;
						}
						delete [] pData;
					}
					if (!success)
					{
						pCallback->OnDLCMountFailed(eDMF_XmlError);
					}
				}
			}
			while(0 == fileFinder->FindNext(nFS, &fd));
			fileFinder->FindClose(nFS);
		}
	}
	pCallback->OnDLCMountFinished( nDLCPacksFound );
}

bool CPlatformOS_PC::CanRestartTitle() const
{
	return false;
}

void CPlatformOS_PC::RestartTitle(const char* pTitle)
{
	CRY_ASSERT_MESSAGE(CanRestartTitle(), "Restart title not implemented (or previously needed)");
}

bool CPlatformOS_PC::UsePlatformSavingAPI() const
{
	// Default true if CVar doesn't exist
	ICVar* pUseAPI = gEnv->pConsole ? gEnv->pConsole->GetCVar("sys_usePlatformSavingAPI") : NULL;
	return !pUseAPI || pUseAPI->GetIVal() != 0;
}

bool CPlatformOS_PC::BeginSaveLoad(unsigned int user, bool bSave)
{
	m_bSaving = bSave;
	return true;
}

void CPlatformOS_PC::EndSaveLoad(unsigned int user)
{
}

IPlatformOS::ISaveReaderPtr CPlatformOS_PC::SaveGetReader(const char* fileName, unsigned int /*user*/)
{
	CSaveReader_CryPakPtr	pSaveReader(new CSaveReader_CryPak(fileName));

	if(!pSaveReader || pSaveReader->LastError() != IPlatformOS::eFOC_Success)
	{
		return CSaveReader_CryPakPtr();
	}
	else
	{
		return pSaveReader;
	}
}

IPlatformOS::ISaveWriterPtr CPlatformOS_PC::SaveGetWriter(const char* fileName, unsigned int /*user*/)
{
	CSaveWriter_CryPakPtr	pSaveWriter(new CSaveWriter_CryPak(fileName));

	if(!pSaveWriter || pSaveWriter->LastError() != IPlatformOS::eFOC_Success)
	{
		return CSaveWriter_CryPakPtr();
	}
	else
	{
		if(m_bLevelLoad)
		{
			m_bSaveDuringLevelLoad = true;
		}

		return pSaveWriter;
	}
}


void CPlatformOS_PC::InitEncryptionKey(const char* pMagic, size_t magicLength, const uint8* pKey, size_t keyLength)
{
	m_encryptionMagic.resize(magicLength);
	memcpy(&m_encryptionMagic[0], pMagic, magicLength);
	m_encryptionKey.resize(keyLength);
	memcpy(&m_encryptionKey[0], pKey, keyLength);
}

void CPlatformOS_PC::GetEncryptionKey(const std::vector<char>** pMagic, const std::vector<uint8>** pKey)
{
	if(pMagic) *pMagic = &m_encryptionMagic;
	if(pKey) *pKey = &m_encryptionKey;
}

/* 
--------------------- Listener -----------------------
*/

void CPlatformOS_PC::AddListener(IPlatformOS::IPlatformListener* pListener, const char* szName)
{
	m_listeners.Add(pListener, szName);
}

IPlatformOS::EMsgBoxResult
CPlatformOS_PC::DebugMessageBox( const char* body, const char* title, unsigned int flags ) const
{
	if (!m_bAllowMessageBox)
		return eMsgBox_OK;

	ICVar *pCVar = gEnv->pConsole ? gEnv->pConsole->GetCVar("sys_no_crash_dialog") : NULL;
	if (pCVar && pCVar->GetIVal() != 0)
	{
		return eMsgBox_OK;
	}


#ifdef PLATFORM_WINDOWS
	int winresult = CryMessageBox( body, title, MB_OKCANCEL );
	return (winresult == IDOK) ? eMsgBox_OK : eMsgBox_Cancel;
#else
	CRY_ASSERT_MESSAGE(false, "DebugMessageBox not implemented on non-windows platforms!");
	return eMsgBox_OK; // [AlexMcC|30.03.10]: Ok? Cancel? Dunno! Uh-oh :( This is only used in CryPak.cpp so far, and for that use it's better to return ok
#endif
}

bool CPlatformOS_PC::PostLocalizationBootChecks()
{
	//Not currently implemented
	return true;
}

bool CPlatformOS_PC::PostBootCheckProcessing()
{
	//Not currently implemented
	return true;
}

bool CPlatformOS_PC::ShowBootCheckMessage(const bool bShow)
{
	CryWarning( VALIDATOR_MODULE_SYSTEM, VALIDATOR_ERROR, "CPlatformOS_PC::ShowBootCheckMessage is not implemented but is being called." );
	return false;
}

void CPlatformOS_PC::SetOpticalDriveIdle(bool bIdle)
{
	//Not currently implemented
}
void CPlatformOS_PC::AllowOpticalDriveUsage(bool bAllow)
{
	//Not currently implemented
}

void CPlatformOS_PC::RemoveListener(IPlatformOS::IPlatformListener* pListener)
{
	m_listeners.Remove(pListener);
}

void CPlatformOS_PC::NotifyListeners(SPlatformEvent& event)
{
	for (CListenerSet<IPlatformOS::IPlatformListener*>::Notifier notifier(m_listeners); notifier.IsValid(); notifier.Next())
		notifier->OnPlatformEvent(event);
}

bool CPlatformOS_PC::KeyboardStart(unsigned int inUserIndex, unsigned int flags, const wchar_t* title, const wchar_t* initialInput, int maxInputLength, IVirtualKeyboardEvents *pInCallback)
{
	return false;
}

bool CPlatformOS_PC::KeyboardIsRunning()
{
	return false;
}

bool CPlatformOS_PC::KeyboardCancel()
{
	return false;
}

bool CPlatformOS_PC::StringVerifyStart(const wchar_t* inString, IStringVerifyEvents *pInCallback)
{
	return false;
}

bool CPlatformOS_PC::IsVerifyingString()
{
	return false;
}

ILocalizationManager::EPlatformIndependentLanguageID CPlatformOS_PC::GetSystemLanguage()
{
	//Not yet implemented
	return ILocalizationManager::ePILID_MAX_OR_INVALID;
}

const char* CPlatformOS_PC::GetSKUId()
{
	//Not yet implemented
	return NULL;
}

ILocalizationManager::TLocalizatonBitfield CPlatformOS_PC::GetSystemSupportedLanguages()
{
	//Not yet implemented
	return 0;
}

void CPlatformOS_PC::GetMemoryUsage(ICrySizer *pSizer) const
{
	pSizer->Add(*this);
	m_listeners.GetMemoryUsage(pSizer);
}

bool CPlatformOS_PC::DebugSave(SDebugDump& dump)
{
	return false;
}

bool CPlatformOS_PC::ConsoleLoadGame(struct IConsoleCmdArgs *pArgs)
{
	return false;
}

int CPlatformOS_PC::GetFirstSignedInUser() const
{
	if(UserIsSignedIn(Unknown_User)) // default user
		return Unknown_User;

	unsigned int maximumSignedInUsers = UserGetMaximumSignedInUsers();
	for (unsigned int user = 0; user < maximumSignedInUsers; ++user)
	{
		if (UserIsSignedIn(user))
		{
			return static_cast<int>(user);
		}
	}

	return Unknown_User;
}

EUserPIIStatus CPlatformOS_PC::GetUserPII(unsigned int inUser, SUserPII *pOutPII)
{
	return k_pii_error;
}

const char* const CPlatformOS_PC::GetHostName()
{
#define HOSTNAME_MAXLENGTH (256)
	static char s_hostnameBuffer[HOSTNAME_MAXLENGTH]={0};
	if (s_hostnameBuffer[0]==0)
	{
		unsigned long int maxLength=HOSTNAME_MAXLENGTH;
		gethostname(s_hostnameBuffer,maxLength);
		s_hostnameBuffer[maxLength-1]=0; // Ensure 0 termination
	}
	return s_hostnameBuffer;
}

IPlatformOS::EZipExtractFail CPlatformOS_PC::ExtractZips( const char* path )
{
	IPlatformOS::EZipExtractFail retVal = eZEF_Success;
	CryLog( "DLCZip: Extracting Downloaded zips" );

	//get the path to the DLC install directory

	char userPath[ICryPak::g_nMaxPath];
	gEnv->pCryPak->AdjustFileName( path, userPath, 0 );
	
	//look for zips
	IPlatformOS::IFileFinderPtr fileFinder = GetFileFinder( 0 );
	_finddata_t fd;
	intptr_t nFS = fileFinder->FindFirst( "%USER%/DLC/*", &fd);
	if (nFS != -1)
	{
		do
		{
			stack_string path;
			path.Format("%s/%s", userPath, fd.name);

			// Skip dirs, only want files, and want them to be zips
			if ( (fd.attrib == _A_SUBDIR) || strstr( fd.name, ".zip" ) == 0 )
			{
				continue;
			}
			
			if (gEnv->pCryPak->IsFileExist(path.c_str()))
			{
				ICryArchive* pArc = gEnv->pCryPak->OpenArchive( path );

				//find the files in the archive...
				ZipDir::CacheRWPtr pZip = static_cast<CryArchiveRW*>( pArc )->GetCache();
				ZipDir::FileEntryTree* pZipRoot = pZip->GetRoot();

				retVal = RecurseZipContents( pZipRoot, userPath, pZip );
			}
		}
		while(0 == fileFinder->FindNext(nFS, &fd) && retVal == eZEF_Success );
		fileFinder->FindClose(nFS);
	}	

	CryLog( "DLCZip: Finished Extracting zips" );

	return retVal;
}

IPlatformOS::EZipExtractFail CPlatformOS_PC::RecurseZipContents( ZipDir::FileEntryTree* pSourceDir, const char* currentPath, ZipDir::CacheRWPtr pCache )
{
	EZipExtractFail retVal = eZEF_Success;

	ZipDir::FileEntryTree::FileMap::iterator fileIt = pSourceDir->GetFileBegin();
	ZipDir::FileEntryTree::FileMap::iterator fileEndIt = pSourceDir->GetFileEnd();

	//look for files and output them to disk
	CryFixedStringT<ICryPak::g_nMaxPath> filePath;
	for( ; fileIt != fileEndIt && retVal == eZEF_Success ; fileIt++ )
	{
		ZipDir::FileEntry* pFileEntry = pSourceDir->GetFileEntry( fileIt );
		const char* pFileName = pSourceDir->GetFileName( fileIt );


		filePath.Format( "%s/%s", currentPath, pFileName );

		void* pData = pCache->AllocAndReadFile( pFileEntry );

		FILE * pNewFile = gEnv->pCryPak->FOpen( filePath.c_str(), "wb" );
		size_t written = gEnv->pCryPak->FWrite( pData, 1, pFileEntry->desc.lSizeUncompressed, pNewFile );

		if( pFileEntry->desc.lSizeUncompressed != written )
		{
			//failed to extract file from zip, report error
			//drop out as fast and cleanly as we can
			retVal = eZEF_WriteFail;
		}

		gEnv->pCryPak->FClose( pNewFile );

		pCache->Free( pData );
	}

	ZipDir::FileEntryTree::SubdirMap::iterator dirIt = pSourceDir->GetDirBegin();
	ZipDir::FileEntryTree::SubdirMap::iterator dirEndIt = pSourceDir->GetDirEnd();

	//look for deeper directories in the heirarchy
	CryFixedStringT<ICryPak::g_nMaxPath> dirPath;
	for( ; dirIt != dirEndIt && retVal == eZEF_Success; dirIt++ )
	{
		dirPath.Format( "%s/%s", currentPath, pSourceDir->GetDirName( dirIt ) );
		gEnv->pCryPak->MakeDir( dirPath );

		retVal = RecurseZipContents( pSourceDir->GetDirEntry( dirIt ), dirPath.c_str(), pCache );
	}

	return retVal;
}

void CPlatformOS_PC::OnSystemEvent( ESystemEvent event,UINT_PTR wparam,UINT_PTR lparam )
{
	switch(event)
	{
	case ESYSTEM_EVENT_LEVEL_LOAD_START:
		m_bLevelLoad = true;
		m_bSaveDuringLevelLoad = false;
		break;
	}
}

void CPlatformOS_PC::OnActionEvent(const SActionEvent& event)
{
	switch(event.m_event)
	{
	case eAE_mapCmdIssued:
		m_bLevelLoad = true;
		m_bSaveDuringLevelLoad = false;
		break;

	case eAE_inGame:
		m_bLevelLoad = false;
		m_fpsWatcher.Reset();
		break;
	}
}

void CPlatformOS_PC::SaveDirtyFiles()
{
	if(m_bSaveDuringLevelLoad)
	{
		// Wait for level load to finish
		if(m_bLevelLoad)
		{
			m_fpsWatcher.Reset();
			return;
		}

		if(!m_fpsWatcher.HasAchievedStableFPS())
		{
			return;
		}

		m_bSaveDuringLevelLoad = false;
		m_bLevelLoad = false;
	}

	if(m_bSaving)
	{
		if(!m_delayLevelStartIcon)
		{
			IPlatformOS::SPlatformEvent event(0);
			event.m_eEventType = IPlatformOS::SPlatformEvent::eET_FileWrite;
			event.m_uParams.m_fileWrite.m_type = SPlatformEvent::eFWT_SaveStart;
			NotifyListeners(event);
		}
		if(m_bSaving && !m_delayLevelStartIcon)
		{
			IPlatformOS::SPlatformEvent event(0);
			event.m_eEventType = IPlatformOS::SPlatformEvent::eET_FileWrite;
			event.m_uParams.m_fileWrite.m_type = IPlatformOS::SPlatformEvent::eFWT_SaveEnd;
			NotifyListeners(event);
			m_bSaving = false;
		}
	}
}

#endif // !defined(XENON) && !defined(PS3)
